Constructors, Destructores, and Assignment Operators
它们属于 non-C part of C++,这些操作是一个 class 的基本操作,几乎每一个 class 都需要实现。应用如下准则之前先确保遵从的是 non-C part of C++。因为一些在较为底层的代码使用构造函数和析构函数会减弱控制性。
Know what functions C++ silently writes and calls
- 默认构造函数
- 复制构造函数
- 移动构造函数
- 复制赋值运算符
- 移动赋值运算符
- 析构函数
在合适的条件下,当没有声明时,编译器会声明,进一步当需要被调用时,编译器会添加实现。但是,如类有 non-static const-qualified or non-static reference type 成员,那么 copy assignment operator 就会被 implicitly-declared=delete
(since C++11),又如基类有 private copy constructor。
Explcitly disallow the use of comiler-generated functions you do not want
情形:当类的要求与这些生成的函数相矛盾时(如不应该被复制)
古法:
- 声明为 private 无法阻止友元,进一步不进行定义导致连接错误;
- 使用一个 Uncopyable 基类,其具有 private 复制构造与复制赋值运算符,protected non-virtual 析构函数。不含数据,符合 empty base class optimization,但多重继承会阻止。优势:提前报错,简洁
现代方法:显式声明为=delete
Declare destructors virtual in polymorphic base classes
多态
此处的“多态”指类通过虚函数支持的的动态多态。
核心是:通过 base class 的统一接口来处理具有相同性质的不同类型的对象。
并不是所有 base class 都有多态的需求。
当 derived class 对象经由 base class 指针被删除,而 base class 带有 non-virtual 析构函数,结果是 UB。实际结果通常是部分销毁。
- 带有 virtual 函数的 class(说明有多态的需要)几乎必须要有一个 virtual 虚构函数。
- 没有多态需求的 base class(没有任何 virtual 函数) 不要有 virtual 析构函数,特别是在内存结构重要时(如小型简单数据,如 Point),因为实现多态需要一个 vptr(动态多态)指向 vtbl 中的实际的函数指针,这会导致额外的储存空间。
阻止继承
C++ 的 final
机制阻止继承
抽象类、纯虚函数
希望创建抽象类,但没有任何纯虚函数时,可以声明一个 pure virtual 析构函数,注意如果析构函数是纯虚函数那么它需要一个定义,因为虚构函数会在对象被销毁时被调用(类似于显式调用)。
Prevent Exceptions from leaving destructors
C++ 不禁止析构函数抛出异常,但不禁止。原因主要在于复杂性,如
- vector 被销毁时每一个元素都要被销毁,如果其中一个销毁出现异常,其他九个仍然应该被释放,如果再来一个异常怎么办?
解决方法:
- abort 终止程序,比 UB 要好
- 吞下。
二者的局限性在于无法对抛出异常的情况进行反应,应该为用户提供一个机会可以处理这样的异常。为用户提供一个手动关闭的接口 close,如果用户没有使用,在析构函数中调用,保证不传播异常或者结束程序。
应该如何界定异常?
Never call virtual functions during construction or destruction
首先直观上在基类构造函数中调用 base class 的虚函数时(该基类构造函数通常由子类的构造函数调用)子类还没有被构造好。析构同理。
实际上,在基类构造/析构函数中虚函数根本不会解析到子类,调用的仍然是基类的虚函数(解析工作是由 linker 执行的)。但是如果不是这两类函数,直接调用和显式的 this-> 调用均会触发正确的虚函数机制。
可以理解为C++标准从行为层面遵从了我们的理解并最大限度地调用尝试完成任务。
将多份类似的构造函数统一为一份,减少出错的概率。
替代方案:
- 在顶层实现统一形式的实现,然后自底向上地传递不同的参数(少了很多虚函数的便利)
- 不使用构造函数
Have assignment oerators return a reference to *this
支持连锁形式
Handle assignment to self in operator=
潜在的自我赋值是有可能发生的,如 a\[i] = a[j]; *px = *py
等。
常见实现:
// not self-assignment-safe, not exception-safe, cause the object data to be ill-formed
Class::operator=(const Class&rhs){
delete p;
p = new Resource(*rhs.p);
return *this;
}
// self-assigment-safe, but not exception-safe, use identity test
Class::operator=(const Class&rhs){
if (rhs == &this)
return *this;
delete p;
p = new Resource(*rhs.p);
return *this;
}
// self-assignment-safe, not exception-safe
Class::operator=(const Class&rhs){
Resource *tmp = new Resource(*rhs.p);
delete p;
p = tmp;
return *this;
}
// both safe
Class::operator=(const Class&rhs){
/*
optional, weigh the possibility of self collision and choose the better one
if (rhs == &this)
return *this;
*/
Resource *old = p;
p = new Resource(&rhs.p);
delete old;
return *this;
}
// both safe, but sometimes not efficient(weight the indirection and the copying to decide). A simple way to achieve exception safety.
Class::operator=(const Class&rhs){
Class tmp {rhs};
std::swap(tmp, *this);
return *this;
}
Class::operator=(Class rhs){
std::swap(rhs, *this);
return *this;
}
Copy all parts of an object
复制构造函数和复制赋值运算符的完整性。
前者会隐式调用基类的默认构造函数而不是复制构造函数,所以需要手动在 member initializer list 中调用基类复制构造函数;后者完全不会默认调用任何基类赋值操作符,也需要手动调用,注意顺序。
手动调用并不是一个个赋初值而是调用基类的复制构造函数/复制赋值运算符。